/*******************************************************************************
 * Copyright (c) 2007, Geophysical Center RAS
 * Sergey Berezin, Dmitry Voitsekhovskiy
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms,
 * with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * - Neither the name of the Geophysical Center RAS nor the names
 * of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;

namespace VisualEsse.DataEngine
{
    /// <summary>
    /// The FilteredArrayStorage stores a two-dimensional array of <typeparamref name="T"/>
    /// and makes a read/write access to it by strided rectangular regions.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class FilteredArrayStorage<T> : IDisposable
        where T : struct
    {
        /************* Storage's File Format Description ***************************
         * FAS (see Signature field)
         * [Type of T: string (full name)]
         * [width : int][height : int]
         * data[0, 0] ... data[0, height - 1]
         * data[1, 0] ...
         * ...
         * data[width - 1, 0] ...
         * *************************************************************************/

        private const string Signature = "FAS";

        private int width;
        private int height;

        private Stream stream;
        private long streamStart = -1;
        private long dataStart = -1;
        private long sizeofT = Marshal.SizeOf(typeof(T));


        /// <summary>
        /// Creates NEW storage and associates it with the specified stream.
        /// </summary>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="stream"></param>
        public FilteredArrayStorage(int width, int height, Stream stream)
        {
            if (width <= 0 || height <= 0)
                throw new ApplicationException("FilteredArrayStorage: width or height has incorrect value!");

            this.stream = stream;
            this.streamStart = stream.Position;

            this.width = width;
            this.height = height;

            Prepare();
        }

        /// <summary>
        /// Reads an EXISTINGS storage from the stream.
        /// </summary>
        /// <param name="stream">Serialized storage.</param>
        public FilteredArrayStorage(Stream stream)
        {
            this.stream = stream;
            this.streamStart = stream.Position;

            Parse();
        }

        public static FilteredArrayStorage<T> OpenOrCreateStorage(int width, int height, Stream stream)
        {
            FilteredArrayStorage<T> storage = null;
            try
            {
                storage = new FilteredArrayStorage<T>(stream);
                if (width != storage.Width || height != storage.Height)
                    throw new ApplicationException("Trying to open the existing storage with different parameters.");
                return storage;
            }
            catch
            {}

            return new FilteredArrayStorage<T>(width, height, stream);
        }

        public void SeekToEnd()
        {
            stream.Position = streamStart + dataStart + (width * height) * sizeofT;
        }

        public int Width { get { return width; } }
        public int Height { get { return height; } }

        /// <summary>
        /// Saves the <paramref name="data"/> to the storage.
        /// </summary>
        /// <param name="iStart">Start index of the data in the entire array.</param>
        /// <param name="iStride">Stride for the first index in the entire array.</param>
        /// <param name="jStart">Start index of the data in the entire array.</param>
        /// <param name="jStride">Stride for the first index in the entire array.</param>
        /// <param name="data">Data to save is a continuous 2d-array which must be stored with specified stride.</param>
        public void Save(int iStart, int iStride, int jStart, int jStride, T[,] data)
        {
            if (typeof(T) == typeof(byte))
            {
                SaveByte(iStart, iStride, jStart, jStride, (byte[,])((Array)data));
                return;
            }

            throw new ApplicationException("FilteredArrayStorage: unsupported generic data type!");
        }

        public void Save(int i, int jStart, int jStride, T[] data)
        {
            if (typeof(T) == typeof(byte))
            {
                SaveByte(i, jStart, jStride, (byte[])((Array)data));
                return;
            }
            else if (typeof(T) == typeof(float))
            {
                SaveFloat(i, jStart, jStride, (float[])((Array)data));
                return;
            }

            throw new ApplicationException("FilteredArrayStorage: unsupported generic data type!");
        }

        private void SaveByte(int i, int jStart, int jStride, byte[] data)
        {
            long position = streamStart + dataStart + (i * height + jStart) * sizeofT;
            stream.Position = position;

            if (jStride == 1)
            {
                int jlen = data.Length;

                for (int j = 0; j < jlen; j++)
                {
                    stream.WriteByte(data[j]);
                }
            }
            else
            {
                int jlen = data.Length;
                long joffset = sizeofT * (jStride - 1);

                for (int j = 0; j < jlen; j++)
                {
                    stream.WriteByte(data[j]);
                    stream.Seek(joffset, SeekOrigin.Current);
                }
            }
        }

        private void SaveFloat(int i, int jStart, int jStride, float[] data)
        {
            long position = streamStart + dataStart + (i * height + jStart) * sizeofT;
            stream.Position = position;

            if (jStride == 1)
            {
                int jlen = data.Length;

                for (int j = 0; j < jlen; j++)
                {
                    WriteFloat(stream, data[j]);
                }
            }
            else
            {
                int jlen = data.Length;
                long joffset = sizeofT * (jStride - 1);

                for (int j = 0; j < jlen; j++)
                {
                    WriteFloat(stream, data[j]);
                    stream.Seek(joffset, SeekOrigin.Current);
                }
            }
        }

        private void SaveByte(int iStart, int iStride, int jStart, int jStride, byte[,] data)
        {
            long position = streamStart + dataStart + (iStart * height + jStart) * sizeofT;
            stream.Position = position;

            if (jStride == 1)
            {
                int ilen = data.GetLength(0);
                int jlen = data.GetLength(1);
                long ioffset = sizeofT * iStride * height;

                for (int i = 0; i < ilen; i++)
                {
                    for (int j = 0; j < jlen; j++)
                    {
                        stream.WriteByte(data[i, j]);
                    }

                    if (i != ilen)
                    {
                        stream.Position = (position += ioffset);
                    }
                }
            }
            else
            {
                int ilen = data.GetLength(0);
                int jlen = data.GetLength(1);
                long ioffset = sizeofT * iStride * height;
                long joffset = sizeofT * (jStride - 1);

                for (int i = 0; i < ilen; i++)
                {
                    for (int j = 0; j < jlen; j++)
                    {
                        stream.WriteByte(data[i, j]);
                        stream.Seek(joffset, SeekOrigin.Current);
                    }

                    if (i != ilen)
                    {
                        stream.Position = (position += ioffset);
                    }
                }
            }
        }

        /// <summary>
        /// Loads the data from the storage.
        /// </summary>
        /// <param name="iStart">Start index of the data in the entire array.</param>
        /// <param name="iStride">Stride for the first index in the entire array.</param>
        /// <param name="iCount">Number of elements by index i to load.</param>
        /// <param name="jStart">Start index of the data in the entire array.</param>
        /// <param name="jStride">Stride for the second index in the entire array.</param>
        /// <param name="jCount">Number of elements by index j to load.</param>
        /// <returns>The requested data - 2d-array of T.</returns>
        public T[,] Load(int iStart, int iStride, int iCount, int jStart, int jStride, int jCount)
        {
            if(typeof(T) == typeof(byte))
            {
                return (T[,])LoadByte(iStart, iStride, iCount, jStart, jStride, jCount);
            }
            //else if (typeof(T) == typeof(float))
            //{
            //    return (T[,])LoadFloat(iStart, iStride, iCount, jStart, jStride, jCount);
            //}
            //else if (typeof(T) == typeof(int))
            //{
            //    return (T[,])LoadInt32(iStart, iStride, iCount, jStart, jStride, jCount);
            //}

            throw new ApplicationException("FilteredArrayStorage: unsupported generic data type!");
        }

        public T[] Load(int i, int jStart, int jStride, int jCount)
        {
            if (typeof(T) == typeof(byte))
            {
                return (T[])LoadByte(i, jStart, jStride, jCount);
            }
            else if (typeof(T) == typeof(float))
            {
                return (T[])LoadFloat(i, jStart, jStride, jCount);
            }

            throw new ApplicationException("FilteredArrayStorage: unsupported generic data type!");
        }

        #region Loaders 

        private Array LoadByte(int i, int jStart, int jStride, int jCount)
        {
            byte[] data = new byte[jCount];

            long position = streamStart + dataStart + (i * height + jStart) * sizeofT;
            stream.Position = position;

            if (jStride == 1)
            {
                for (int j = 0; j < jCount; j++)
                {
                    data[j] = (byte)stream.ReadByte();
                }

            }
            else
            {
                long joffset = sizeofT * (jStride - 1);

                for (int j = 0; j < jCount; j++)
                {
                    data[j] = (byte)stream.ReadByte();
                    stream.Seek(joffset, SeekOrigin.Current);
                }
            }

            return data;
        }

        private void WriteFloat(Stream s, float f)
        {
            byte b1 ;
            byte b2 ;
            byte b3 ;
            byte b4 ;

            unsafe
            {
                float* d = &f;
                byte* d2 = (byte*)d;
                b1 = d2[0];
                b2 = d2[1];
                b3 = d2[2];
                b4 = d2[3];
            }

            s.WriteByte(b1);
            s.WriteByte(b2);
            s.WriteByte(b3);
            s.WriteByte(b4);
        }


        private float ReadFloat(Stream s)
        {
            byte b1 = (byte)s.ReadByte();
            byte b2 = (byte)s.ReadByte();
            byte b3 = (byte)s.ReadByte();
            byte b4 = (byte)s.ReadByte();
            float f = 0.0f;

            unsafe
            {
                float* d = &f;
                byte* d2 = (byte*)d;
                d2[0] = b1;
                d2[1] = b2;
                d2[2] = b3;
                d2[3] = b4;
            }

            return f;
        }

        private Array LoadFloat(int i, int jStart, int jStride, int jCount)
        {
            float[] data = new float[jCount];

            long position = streamStart + dataStart + (i * height + jStart) * sizeofT;
            stream.Position = position;

            if (jStride == 1)
            {
                for (int j = 0; j < jCount; j++)
                {
                    data[j] = ReadFloat(stream);
                }

            }
            else
            {
                long joffset = sizeofT * (jStride - 1);

                for (int j = 0; j < jCount; j++)
                {
                    data[j] = ReadFloat(stream);
                    stream.Seek(joffset, SeekOrigin.Current);
                }
            }

            return data;
        }

        private Array LoadByte(int iStart, int iStride, int iCount, int jStart, int jStride, int jCount)
        {
            byte[,] data = new byte[iCount, jCount];

            long position = streamStart + dataStart + (iStart * height + jStart) * sizeofT;
            stream.Position = position;

            if (jStride == 1)
            {
                long ioffset = sizeofT * iStride * height;

                for (int i = 0; i < iCount; i++)
                {
                    for (int j = 0; j < jCount; j++)
                    {
                        data[i,j] = (byte)stream.ReadByte();
                    }

                    if (i != iCount)
                    {
                        stream.Position = (position += ioffset);
                    }
                }
            }
            else
            {
                long ioffset = sizeofT * iStride * height;
                long joffset = sizeofT * (jStride - 1);

                for (int i = 0; i < iCount; i++)
                {
                    for (int j = 0; j < jCount; j++)
                    {
                        data[i, j] = (byte)stream.ReadByte();
                        stream.Seek(joffset, SeekOrigin.Current);
                    }

                    if (i != iCount)
                    {
                        stream.Position = (position += ioffset);
                    }
                }
            }

            return data;
        }

        

        #endregion 

        private void Prepare()
        {
            stream.Seek(streamStart, SeekOrigin.Begin);
            BinaryWriter writer = new BinaryWriter(stream);

            // Writing the signature
            writer.Write(Signature.ToCharArray());

            // Writing the parameters
            writer.Write(typeof(T).FullName);
            writer.Write(width);
            writer.Write(height);

            dataStart = stream.Position;
        }

        private void Parse()
        {
            stream.Seek(streamStart, SeekOrigin.Begin);
            BinaryReader reader = new BinaryReader(stream);

            // Reading the signature
            char[] chars = reader.ReadChars(Signature.Length);
            if (Signature.CompareTo(new string(chars)) != 0)
                throw new ApplicationException("FilteredArrayStorage: the signature is missing or incorrect!");

            // Reading parameters
            string typeString = reader.ReadString();
            if(typeString != typeof(T).FullName)
                throw new ApplicationException("FilteredArrayStorage: the storage cannot be used for the element type other than it has been created for!");

            width = reader.ReadInt32();
            height = reader.ReadInt32();

            if(width <= 0 || height <= 0)
                throw new ApplicationException("FilteredArrayStorage: width or height has incorrect value!");

            dataStart = stream.Position;
        }

        #region IDisposable Members

        public void Dispose()
        {
            if (stream != null)
            {
                stream.Dispose();
                stream = null;
            }
        }

        #endregion
    }
}
